package com.ysw.experience.缓存击穿_穿透;

import com.ysw.spring.spring_util.json.JsonUtils;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

/**
 * @ClassName RedisUtil
 * @Description 缓存击穿及穿透解决方案工具包封装
 * @Author ysw
 * @Date 2022/5/12 0:27
 */
@Component
public class RedisUtil {

    private final StringRedisTemplate stringRedisTemplate;

    @Autowired
    private RedissonClient redissonClient;

    /**
     * 可以使用构造函数处理
     *
     * @param stringRedisTemplate
     */
    public RedisUtil(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 缓存穿透：数据库不存在数据并且redis中不存在数据，数据访问全都落在数据库
     * 解决方案
     * 1. bloomFilter  使用布隆算法解决
     * 2. 对不存在的数据设置空存储
     */
    /**
     * 缓存穿透解决方式抽取
     *
     * @param redisKey redis key
     * @param param    入参，及根据查库条件
     * @param rType    返回值类型
     * @param expire   过期时间
     * @param timeUnit 时间单位 SECOND OR MILLISECONDS
     * @param callback 回调方法
     * @param <R>      返回值
     * @param <P>      参数值
     * @return R
     */
    public <R, P> R queryWithPassThrough(String redisKey, P param, Class<R> rType, Long expire, TimeUnit timeUnit, Function<P, R> callback) {
        BoundValueOperations<String, String> operations = stringRedisTemplate.boundValueOps(redisKey);
        String jsonValue = operations.get();
        // 存在缓存数据
        if (StringUtils.isNotBlank(jsonValue)) {
            return JsonUtils.stringToObject(jsonValue, rType);
        }
        // 判断命中的是否为不存在的key 类似于 "".equals(jsonValue.trim())
        if (null != jsonValue) {
            return null;
        }
        // 使用函数式对外暴露
        R apply = callback.apply(param);
        if (null == apply) {
            operations.set("", expire, timeUnit);
            return null;
        }
        operations.set(JsonUtils.objectToString(apply), expire, timeUnit);
        return apply;
    }

    /**
     * 缓存穿透解决公共类，数据一致性要求高的应用场景.
     * @param redisKey rediskey
     * @param param  查询id
     * @param rClazz 返回对象class
     * @param unit  对象过期 unit
     * @param expire  过期时间
     * @param apply  对外暴露方法
     * @param isSupportPassThrow 是否支持缓存穿透
     * @param <R>  返回值
     * @param <P>  参数
     * @return
     */
    public <R, P> R queryWithPenetration(String redisKey, P param, Class<R> rClazz, TimeUnit unit, Long expire, Function<P, R> apply, Boolean isSupportPassThrow) {
        BoundValueOperations<String, String> boundValueOps = stringRedisTemplate.boundValueOps(redisKey);
        String jsonValue = boundValueOps.get();
        // 缓存命中,存在缓存数据
        if (StringUtils.isNotBlank(jsonValue)) {
            System.out.println(Thread.currentThread().getName() + "-缓存命中：返回");
            return JsonUtils.stringToObject(jsonValue, rClazz);
        }
        // 解决缓存穿透
        if (null != jsonValue && isSupportPassThrow) {
            return null;
        }
        R r = apply.apply(param);
        // 解决缓存穿透，赋空值
        if (null == r && isSupportPassThrow) {
            boundValueOps.set("");
            return null;
        }
        RLock lock = null;
        try {
            lock = redissonClient.getLock(RedisKeyConstant.PREFFIX_KEY + redisKey);
            // 这里最好添加 过期时间防止死锁
            boolean isLock = lock.tryLock(0, 20, TimeUnit.SECONDS);
            System.out.println(Thread.currentThread().getName() + "-加锁状态：" + isLock);
            if (isLock) {
                String doubleCheckValue = boundValueOps.get();
                // 缓存命中,存在缓存数据
                if (StringUtils.isNotBlank(doubleCheckValue)) {
                    System.out.println(Thread.currentThread().getName() + "-double check：返回");
                    return JsonUtils.stringToObject(doubleCheckValue, rClazz);
                }
                boundValueOps.set(JsonUtils.objectToString(r), expire, unit);
                return r;
            } else {
                TimeUnit.MILLISECONDS.sleep(50);
                System.out.println(Thread.currentThread().getName() + "-重试：返回");
                // 进行重试的时候就不需要 缓存穿透
                queryWithPenetration(redisKey, param, rClazz, unit, expire, apply, false);
            }
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        } finally {
            // 判断当前线程是否持有锁，如果不判断，在tryLock= false 的情况下，直接lock的话，回抛出异常
           /* System.out.println(Thread.currentThread().getName()+ "-持isLocked状态：" +lock.isLocked()+
                    "\nisHeldByCurrentThread状态:" + lock.isHeldByCurrentThread() );*/
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
                System.out.println(Thread.currentThread().getName() + "- 解锁成功：返回");
            }
        }
        return r;
    }

    /**
     * 缓存击穿解决方式二，一致性较弱，但是性能高 AP
     * @param redisKey redisKey
     * @param param  参数
     * @param unit  时间单元
     * @param expire 过期时间
     * @param callback  回调
     * @param threadPoll 线程吃
     * @param <P> 入参
     * @param <R> 出参
     * @return
     */
    public <P,R> R queryWithLogicData(String redisKey,
                                      P param,
                                      TimeUnit unit,
                                      Long expire,
                                      Function<P, R> callback,
                                      ThreadPoolExecutor threadPoll){
        BoundValueOperations<String, String> boundValueOps = stringRedisTemplate.boundValueOps(redisKey);
        String jsonValue = boundValueOps.get();
        // 缓存未命中，直接返回,因为是热点数据，所以就不用去考虑其他问题，
        // 缓存如果没有，那么他就是不在热点范围内的数据
        if (StringUtils.isBlank(jsonValue)) {
            return null;
        }
        LogicExpireData logicExpireData = JsonUtils.stringToObject(jsonValue, LogicExpireData.class);
        long now = System.currentTimeMillis();
        Long expireTime = logicExpireData.getExpireTime();
        if(expireTime > now){
            return (R)logicExpireData.getData();
        }
        RLock lock;
        try {
            lock = redissonClient.getLock(RedisKeyConstant.REDISSON_LOGIC_PREFIX + String.valueOf(param));
            boolean isLock = lock.tryLock(0, 20, TimeUnit.SECONDS);
            // 同步中开启线程异步去修改缓存信息
            if(isLock){
                RLock finalLock = lock;
                Runnable task = () -> {
                    try {
                        long newExpireTime = now + TimeUnit.MILLISECONDS.convert(expire, unit);
                        R data = callback.apply(param);
                        LogicExpireData newData = new LogicExpireData().setData(data).setExpireTime(newExpireTime);
                        boundValueOps.set(JsonUtils.objectToString(newData));
                    } finally {
                        // 释放锁
                        if (null != finalLock && finalLock.isHeldByCurrentThread()) {
                            finalLock.unlock();
                        }
                    }
                };
                threadPoll.submit(task);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return (R)logicExpireData.getData();
    }
}